package main

// ===============================================================================================================================
// 本工具的主要特点如下：
// 一、灵活性，具备简单的测试数据生成能力，结合系统配置的业务数据和动态数据，可以满足大部份的业务测试场景
// 二、易用性，不需要编程即可发起性能测试，既可作为流水线级别的测试，也可作为专家级的性能测试分析工具
// 三、高性能，单机性能达到C语言级别，占用少量的压力机资源，即可到达万门级的并发测试能力
// 四、云端测试，本工具会完善云端管理工具，可以在云端发起压力测试，尽可能真实的模似真实的生产环境
// 五、资源监控，安装本工具的资源监控软件，可以在云端看到各个节点的服务器资源消耗情况，轻松完成性能评估
//================================================================================================================================
// 关于nb工具未来的一些设想
// 监控java内部程序性能，可以分析网络调用、数据库传输、网络传输性能，实现快速性能问题的定位和分析
// 添加更多语言的agent,分析程序内部性能
// 增加web端的性能测试
// ....
//=================================================================================================================================
// 本工具有码农知道开源组织发起
// 作者：李声波
//=================================================================================================================================
import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"runtime"
	"strconv"
	"sync"
	"time"
)

//参数
var (
	data                string
	chanCount           int64  //单个协程请求总数
	totalCount          int64  //所有协程请求总数
	chanSuccessCount    int64  //单个协程成功数
	totalSuccessCount   int64  //所有协程成功总数
	chanFaultCount      int64  //单个故障数
	totalChanFaultCount int64  //所有协程故障数
	chanTotalTime       uint64 //单个协程请求总耗时
	totalTime           uint64
	avgTime             float64 //平均响应时间
	qps                 float64 //qps
	c                   int
	n                   int
	t                   string
	u                   string
	f                   string
	d                   string
	g                   bool
	h                   bool //显示帮助信息
)

//测试结果结构体
type results struct {
	requestTime  uint64 //单个请求时间
	isSuccess    bool   //请求是否成功
	responseText string //响应文本
}

//Get请求
func get(url string) string {
	client := &http.Client{Timeout: 5 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var buffer [512]byte
	result := bytes.NewBuffer(nil)
	for {
		n, err := resp.Body.Read(buffer[0:])
		result.Write(buffer[0:n])
		if err != nil && err == io.EOF {
			break
		} else if err != nil {
			panic(err)
		}
	}
	return result.String()
}

//  发送POST请求
// data:  请求的数据
// contentType： 请求体格式，如：application/json
// n     请求数
// b     打印调式信息
// content：     请求放回的内容
// out：输入信道
func Post(url string, data string, contentType string, n int, b bool, out chan<- results, send *sync.WaitGroup) {
	defer func() {
		send.Done()
	}()
	var Results1 results
	client := &http.Client{Timeout: 5 * time.Second}
	message := make(map[string]interface{})

	//jsonStr, _ := json.Marshal(data)
	var datab = []byte(data)
	json.Unmarshal(datab, &message)

	//请求次数
	for i := 0; i < n; i++ {
		//2020-7-13号每500条打印一次结果可以解决协程下字符截取报错的问题，相当于加了一点点睡眠，这TM的和C语言一样啊
		message["seriaNo"] = "H" + string(createCaptcha(16))
		message["txnAmt"] = createCaptcha(4)
		message["uniqueId"] = "00" + strconv.Itoa(i)
		message["loanPurpose"] = generateSubID()
		//重新序列化成JSON字符串
		jsonStr, _ := json.Marshal(message)
		if b == true {
			fmt.Println(string(jsonStr))
		}
		//请求开始时间
		startTime := time.Now()
		resp, err := client.Post(url, contentType, bytes.NewBuffer(jsonStr))

		//2020-7-13 修复高并发下，HTTP请求报错的问题
		if err != nil {
			if b == true {
				fmt.Println(err)
			}
			Results1.isSuccess = false //设置http请求状态
			Results1.responseText = string(err.Error())
		} else {
			//请求成功才能关闭请求，不然会报错滴
			defer resp.Body.Close()
			if resp.StatusCode == 200 {
				Results1.isSuccess = true //设置http请求状态
			}
			//_表示忽略返回值
			result, _ := ioutil.ReadAll(resp.Body)
			Results1.requestTime = uint64(diffNano(startTime)) //单个请求结束时间
			Results1.responseText = string(result)             //设置响应报文
		}

		out <- Results1 //将结果输入给信道
	}

	//关闭信道
	//close(out)
	//return
}

//C 并发数
func chStatistics(in <-chan results, c float64, b bool, received *sync.WaitGroup) {
	defer func() {
		received.Done()
	}()
	for i := range in {
		chanTotalTime += i.requestTime
		chanCount++ //请求总数
		if i.isSuccess == true {
			chanSuccessCount++
		} else if i.isSuccess == false {
			chanFaultCount++
		}
		if b == true {
			GID := strconv.Itoa(int(getGID()))
			fmt.Println("协程ID:" + GID + " 响应时间：" + strconv.Itoa(int(i.requestTime)) + "ms 响应报文:" + i.responseText + "是否成功:" + strconv.FormatBool(i.isSuccess))
		}
		if chanCount%500 == 0 {
			//非离散加权平均 纳纱转毫毛的计算方法 chanTotalTime%1e6/1e3
			avgTime = nanoToMillisecond(chanTotalTime) / float64(chanCount)
			//qps = float64(chanCount) / (float64(avgTime) / 1e3)
			//执行一段时间，处理的请求数，先算数出执行所有请求需要的平均时间，再用总时间除以平均时间 float64((chanTotalTime/1e9/60))/
			qps = (float64(chanCount) / (nanoToMillisecond(chanTotalTime) / c / 1e3))
			fmt.Printf("已经完成%d个请求-------------------->  成功数:%d  失败数:%d  QPS/每秒:%8.2f  平均响应时间/MS:%8.4f\r\n", chanCount, chanSuccessCount, chanFaultCount, qps, avgTime)
		}
	}

	totalCount += chanCount
	totalSuccessCount += chanSuccessCount
	totalChanFaultCount += chanFaultCount
	totalTime += chanTotalTime
	//非离散加权平均
	avgTime = nanoToMillisecond(totalTime) / float64(totalCount)
	//qps = float64(totalCount) / (float64(avgTime) / 1e3)
	//执行一段时间，处理的请求数，先算数出执行所有请求需要的平均时间，再用总时间除以平均时间
	qps = (float64(totalCount) / (nanoToMillisecond(totalTime) / c / 1e3))
	fmt.Println("")
	fmt.Println("----------------------------------- START---------------------------------------------------------")
	fmt.Println("执行完成，报告如下")
	fmt.Println("本报告由nb工具出具，联系邮箱：397237738@qq.com，联系电话：保密")
	fmt.Println("")
	fmt.Printf("并发数:%d\t\n", int(c))
	fmt.Printf("请求总数:%d  成功数: %d  失败数:%d  QPS/每秒:%8.2f  平均响应时间/MS:%8.2f  请求总时间(S)：%8.2f\r\n", totalCount, totalSuccessCount, totalChanFaultCount, qps, avgTime, nanoToMillisecond(totalTime)/c/1e3)
	fmt.Println("-------------------------------------END----------------------------------------------------------")
}

// 生成随机数的方法
func createCaptcha(bit int) string {
	time.Sleep(200) //延时200纳秒，防止高并发执行时随机数重复，并且出现切片错误
	s := fmt.Sprintf("%d", rand.New(rand.NewSource(time.Now().UnixNano()+int64(getGID()))).Int63())
	return subString(s, 0, bit)
}
func getGID() uint64 {
	b := make([]byte, 64)
	b = b[:runtime.Stack(b, false)]
	b = bytes.TrimPrefix(b, []byte("goroutine "))
	b = b[:bytes.IndexByte(b, ' ')]
	n, _ := strconv.ParseUint(string(b), 10, 64)
	return n
}
func subString(str string, begin, length int) (substr string) {
	// 将字符串的转换成[]rune
	rs := []rune(str)
	lth := len(rs)
	// 简单的越界判断
	if begin < 0 {
		begin = 0
	}
	if begin >= lth {
		begin = lth
	}
	end := begin + length
	if end > lth {
		end = lth
	}
	// 返回子串
	return string(rs[begin:end])
}

//生成投取数组中的字符，用于测试贷款原因
//以后需要封装成通过表达式
//谁来实现?
func generateSubID() string {
	var pl = [18]string{"PL01", "PL02", "PL03", "PL04", "PL05", "PL06", "PL07", "PL08", "PL99", "PL14", "PL15", "PL16", "PL17", "PL18", "PL19", "PL20", "PL21", "PL22"}
	i := rand.Intn(18)

	/**var letterRunes = []rune("PL02")
	b := make([]rune, bit)
	for i := range b {
		b[i] = letterRunes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(letterRunes))]
	}**/
	return string(pl[i])
}

// 计算时间差，纳秒
func diffNano(startTime time.Time) (diff int64) {
	startTimeStamp := startTime.UnixNano()
	endTimeStamp := time.Now().UnixNano()
	diff = (endTimeStamp - startTimeStamp)
	return
}

//纳稍转毫秒
func nanoToMillisecond(time1 uint64) float64 {
	return float64(time1 / 1e6)
}

func testData() string {
	jsonStr := `
	{
		"seriaNo": "103120200401001",
		"uniqueId": "001",
		"contractNo": "MS20200401001",
		"name": "刘备11",
		"identType": "I",
		"certNo": "151026579012830",
		"validity": "20190101-长期",
		"phoneNo": "18100000000",
		"telNo": "18100000000",
		"postCode": "100000",
		"postAddr": "北京",
		"homeCode": "100000",
		"projNo": "1000004394",
		"prdtNo": "1031001",
		"lnRate": "2",
		"payType": "01",
		"bankCode": "007",
		"applyTime": "2020022110",
		"txnAmt": "3000",
		"loanTerm": "3",
		"loanPurpose": "PL16",
		"contactName": "张三",
		"bankCardNo": "6226090000000048",
		"bankName": "招商银行",
		"residentialAddress": "北京",
		"gender": "M",
		"national": "CHN",
		"profession": "3",
		"creditInfo": [    {}
		]
	  }
	`
	return jsonStr
}

//读取文件
func readFile(file string) string {
	data, err := ioutil.ReadFile("file")
	if err != nil {
		fmt.Println("read file err:", err.Error())
	}
	return string(data)
}
func init() {
	flag.IntVar(&c, "c", 1, "并发数")
	flag.IntVar(&n, "n", 1, "请求次数")
	flag.StringVar(&t, "t", "application/json", "http请求类型，例如：application/json")
	flag.StringVar(&u, "u", "", "http请求地址")
	flag.StringVar(&f, "f", "", "http请求数据，使用txt文件存储")
	flag.StringVar(&d, "d", "", "请求数据，如果是JSON格式，建议使用-f参数传值")
	flag.BoolVar(&g, "g", false, "打印调试信息")
	flag.BoolVar(&h, "h", false, "显示帮助信息")
}
func main() {
	flag.Parse()

	if h == true {
		fmt.Println("-c 并发数")
		fmt.Println("-n 请求次数")
		fmt.Println("-t http请求类型，例如：application/json")
		fmt.Println("-u http请求地址")
		fmt.Println("-f http请求数据，使用txt文件存储")
		fmt.Println("-d 请求数据，如果是JSON格式，建议使用-f参数传值")
		fmt.Println("-g 打印调试信息")
		fmt.Println("-h 显示帮助信息")
		return
	}
	if u == "" {
		fmt.Println("请求地址不能为空")
		fmt.Println("输入-h显示帮助信息")
		return
	}
	fmt.Println("正在对 " + u + " 进行测试，请等待...")
	if d != "" && f != "" {
		fmt.Println("-f和-d参数只能使用一个，不能同时使用")
		return
	}
	if f != "" {
		data = readFile(f)
	} else {
		data = testData()
	}
	if d != "" {
		data = d
	}

	//声明信道
	ch := make(chan results, 1000)
	var (
		send     sync.WaitGroup // 发送数据完成
		received sync.WaitGroup // 数据处理完成
	)
	received.Add(1)
	//统计请求结果
	go chStatistics(ch, float64(c), g, &received)

	for i := 0; i < c; i++ {
		send.Add(1)
		//并发执行
		go Post(u, data, t, n, g, ch, &send)

	}
	// 等待所有的数据都发送完成
	send.Wait()
	// 延时1毫秒 确保数据都处理完成了
	time.Sleep(1 * time.Millisecond)

	close(ch)
	// 数据全部处理完成了
	received.Wait()

	//testResult()
}
